/*
 * Copyright 2009-2010 the original author or authors.
 *
 * Licensed under the Apache license, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.internna.iwebjtracker.subversion.server;

import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.HashMap;
import java.util.Map.Entry;
import java.util.ArrayList;
import java.util.Collections;
import java.io.ByteArrayOutputStream;
import java.io.UnsupportedEncodingException;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.internna.iwebjtracker.model.TrackerUser;
import org.internna.iwebjtracker.model.ItemDataImpl;
import org.internna.iwebjtracker.model.DefaultModule;
import org.internna.iwebjtracker.model.DefaultProject;
import org.internna.iwebjtracker.model.DefaultVersion;
import org.internna.iwebjtracker.server.scm.model.ItemData;
import org.internna.iwebjtracker.IWebJTrackerBootException;
import org.internna.iwebjtracker.model.DefaultConnectionDetails;
import org.internna.iwebjtracker.model.UsernamePasswordCredentials;
import org.internna.iwebjtracker.server.connection.ConnectionDetails;
import org.internna.iwebjtracker.subversion.server.model.SVNKitChange;
import org.internna.iwebjtracker.subversion.server.model.SVNKitLogEntry;
import org.internna.iwebjtracker.subversion.server.model.SVNKitRevision;
import org.internna.iwebjtracker.subversion.server.model.SVNKitItemInfo;
import org.internna.iwebjtracker.server.scm.AbstractSourceControlManagementServer;
import org.internna.iwebjtracker.subversion.server.connection.SVNKitWrapperConnection;
import org.tmatesoft.svn.core.SVNURL;
import org.tmatesoft.svn.core.SVNLogEntry;
import org.tmatesoft.svn.core.SVNException;
import org.tmatesoft.svn.core.wc.SVNWCUtil;
import org.tmatesoft.svn.core.wc.SVNRevision;
import org.tmatesoft.svn.core.wc.SVNWCClient;
import org.tmatesoft.svn.core.wc.SVNLogClient;
import org.tmatesoft.svn.core.ISVNLogEntryHandler;
import org.tmatesoft.svn.core.wc.SVNClientManager;
import org.tmatesoft.svn.core.internal.wc.DefaultSVNOptions;
import org.tmatesoft.svn.core.internal.io.dav.DAVRepositoryFactory;
import org.tmatesoft.svn.core.internal.io.svn.SVNRepositoryFactoryImpl;

import static org.internna.iwebmvc.utils.StringUtils.hasText;

/**
 * Subversion server implementation based on SVNKit (http://svnkit.com/).
 *
 * @author Jose Noheda
 * @since 1.0
 */
public final class SVNKitSubversionServerImpl extends AbstractSourceControlManagementServer<SVNKitWrapperConnection, DefaultConnectionDetails, DefaultModule, DefaultVersion, TrackerUser, DefaultProject, SVNKitItemInfo, SVNKitRevision, SVNKitChange, SVNKitLogEntry> implements SubversionServer {

    private static final String SVN_PROTOCOL = "svn";
    private static final Log logger = LogFactory.getLog(SVNKitSubversionServerImpl.class);

    private final DefaultSVNOptions options;
    private final Map<ConnectionDetails, SVNClientManager> repositories;

    public SVNKitSubversionServerImpl() {
        super();
        addSupportedProtocol(SVN_PROTOCOL);
        try {
            DAVRepositoryFactory.setup();
            SVNRepositoryFactoryImpl.setup();
            options = SVNWCUtil.createDefaultOptions(true);
        } catch (RuntimeException e) {
            throw new IWebJTrackerBootException("Could not instantiate SVN server", e);
        }
        repositories = new HashMap<ConnectionDetails, SVNClientManager>();
    }

    /**
     * Closes all active connections and clears all repositories.
     */
    public void destroy() {
        for (Entry<ConnectionDetails, SVNClientManager> manager : repositories.entrySet()) {
            manager.getValue().dispose();
        }
        repositories.clear();
    }

    private SVNClientManager obtainRepository(DefaultConnectionDetails connectionDetails) {
        UsernamePasswordCredentials credentials = connectionDetails == null ? null : connectionDetails.getUsernamePasswordCredentials();
        return obtainRepository(credentials);
    }

    private synchronized SVNClientManager obtainRepository(UsernamePasswordCredentials credentials) {
        return credentials == null ? SVNClientManager.newInstance(options) : SVNClientManager.newInstance(options, credentials.getUsername(), credentials.getPassword());
    }

    private synchronized SVNWCClient getOrCreateWCClientFromConnection(SVNKitWrapperConnection connection) {
        SVNWCClient client = connection == null ? null : connection.getWcClient();
        if (client == null) {
            if (connection.getManager() == null) {
                connection.setManager(obtainRepository(connection.getConnectionDetails()));
            }
            client = connection.getManager().getWCClient();
            connection.setWcClient(client);
        }
        return client;
    }

    /**
     * Gets a connection manager but does not actually open any physical connection until it's needed.
     */
    @Override protected SVNKitWrapperConnection doOpenConnection(DefaultConnectionDetails connectionDetails) {
        SVNKitWrapperConnection connection = null;
        if (connectionDetails != null) {
            String url = connectionDetails.getUrl();
            try {
                connection = new SVNKitWrapperConnection();
                connection.setConnectionDetails(connectionDetails);
                connection.setRepositoryURL(SVNURL.parseURIDecoded(url));
                connection.setManager(obtainRepository(connectionDetails));
            } catch (SVNException ex) {
                if (logger.isDebugEnabled()) {
                    logger.debug("Could not open connection to [" + url + "]: " + ex.getMessage());
                }
            }
        }
        return connection;
    }

    /**
     * Gets the item info for the URL used in the connection details and checks that it's a repository root URL.
     */
    @Override protected boolean doTestConnection(SVNKitWrapperConnection connection) {
        SVNKitItemInfo info = getItemInfo(connection, null, null);
        if (logger.isDebugEnabled()) {
            logger.debug("Testing SVN connection obtained: " + (info != null && info.isRepositoryURL() ? "URL[" + info.getUrl() + "] is a repository root" : "URL error!"));
        }
        return info != null && info.isRepositoryURL();
    }

    /**
     * Releases the reference to the SVNClientManager instance.
     *
     * @param connection any
     */
    @Override protected void doCloseConnection(SVNKitWrapperConnection connection) {
        if (connection != null) {
            connection.close();
        }
    }

    /**
     * Obtains the item info from the parsed URL. The parsed URL is the connection URL unless the project has a valid URL.
     * The absolute path parameter is only appended to project relative URLs.
     *
     * @param connection a non null connection
     * @param project any object
     * @param absolutePath usually null or a string that starts with /
     * @return null if the item form the parsed URL does not exist. Otherwise the available info
     */
    @Override protected SVNKitItemInfo getItemInfo(SVNKitWrapperConnection connection, DefaultProject project, String absolutePath) {
        SVNURL url = connection.getRepositoryURL();
        try {
            if (project != null) {
                String urlData = project.getSourceControlURL();
                if (hasText(absolutePath)) {
                    urlData += absolutePath;
                }
                url = SVNURL.parseURIDecoded(urlData);
            }
            if (logger.isDebugEnabled()) {
                logger.debug("Accessing subversion to obtain item information for [" + url + "]");
            }
            SVNWCClient client = getOrCreateWCClientFromConnection(connection);
            return new SVNKitItemInfo(client.doInfo(url, null, SVNRevision.HEAD), project);
        } catch (Exception ex) {
            if (logger.isDebugEnabled()) {
                logger.debug("Could not obtain item info for [" + url + "]: " + ex.getMessage());
            }
        }
        return null;
    }

    /**
     * Access subversion to read the contents.
     *
     * @param item a valid item
     * @return the contents (ascii or binary)
     */
    @Override public ItemData loadItem(SVNKitWrapperConnection connection, DefaultProject project, String url, boolean binary) {
        if ((project != null) && hasText(url) && !binary) {
            return new ItemDataImpl(cat(connection, project.getSourceControlURL() + "/" + url));
        }
        return null;
    }

    /**
     * Delegates to cat(SVNKitWrapperConnection connection, SVNURL url, SVNRevision revision) assuming HEAD.
     */
    @Override public String cat(SVNKitWrapperConnection connection, String url) {
        try {
            return hasText(url) ? cat(connection, SVNURL.parseURIEncoded(url)) : null;
        } catch (SVNException ex) {
            if (logger.isDebugEnabled()) {
                logger.debug("Could not parse URL string [" + url + "] to a valid SVN URL. Does it need encoding?");
            }
            return null;
        }
    }

    /**
     * Delegates to cat(SVNKitWrapperConnection connection, SVNURL url, SVNRevision revision) assuming HEAD.
     */
    @Override public String cat(SVNKitWrapperConnection connection, SVNURL url) {
        return cat(connection, url, SVNRevision.HEAD);
    }

    /**
     * Access the repository and gets the file contents.
     */
    @Override public String cat(SVNKitWrapperConnection connection, SVNURL url, SVNRevision revision) {
        String contents = null;
        if ((url != null) && (connection != null)) {
            SVNWCClient client = getOrCreateWCClientFromConnection(connection);
            if (client != null) {
                ByteArrayOutputStream stream = readFile(client, url, revision == null ? SVNRevision.HEAD : revision);
                try {
                    contents = stream.toString("UTF-8");
                } catch (UnsupportedEncodingException ex) {
                    if (logger.isDebugEnabled()) {
                        logger.debug("Exception converting text file " + url + " retrieved from Subversion: " + ex.getMessage());
                    }
                }
            }
        }
        return contents;
    }

    private ByteArrayOutputStream readFile(SVNWCClient client, SVNURL url, SVNRevision revision) {
        ByteArrayOutputStream stream = new ByteArrayOutputStream();
        try {
            if (logger.isDebugEnabled()) {
                logger.debug("Accessing subversion to read text file contents from [" + url + "]");
            }
            client.doGetFileContents(url, revision, revision, false, stream);
        } catch (SVNException ex) {
            if (logger.isDebugEnabled()) {
                logger.debug("Exception obtaining file " + url + " from Subversion: " + ex.getMessage());
            }
        }
        return stream;
    }

    /**
     * Access Subversion and asks for the log between two revisions (limits to 10 the number of entries for performance reasons)
     *
     * @param connection any
     * @param from any
     * @param to any
     * @return
     */
    @Override public List<SVNKitLogEntry> log(SVNKitWrapperConnection connection, DefaultProject project, SVNKitRevision from, SVNKitRevision to) {
        final List<SVNKitLogEntry> entries = new ArrayList<SVNKitLogEntry>();
        if ((connection != null) && (project != null) && (from != null)) {
            SVNLogClient client = connection.getLogClient();
            if (client != null) {
                try {
                    client.doLog(SVNURL.parseURIDecoded(project.getSourceControlURL()), new String[] {"/"}, from.getRevision(), from.getRevision(), to.getRevision(), false, true, 10, new ISVNLogEntryHandler() {
                        @Override public void handleLogEntry(SVNLogEntry logEntry) throws SVNException {
                            SVNKitLogEntry entry = new SVNKitLogEntry(logEntry);
                            entries.add(entry);
                            if (logger.isDebugEnabled()) {
                                logger.debug("Retrieved log entry from Subversion [" + entry + "]");
                            }
                        }
                    });
                } catch (SVNException ex) {
                    if (logger.isDebugEnabled()) {
                        logger.debug("Error getting log entries for [" + project.getAlias() + "]: " + ex.getMessage());
                    }
                }
            }
        }
        return Collections.unmodifiableList(entries);
    }

    /**
     * Server type is Subversion.
     *
     * @return "Subversion"
     */
    @Override public String getServerType() {
        return "Subversion";
    }

    @Override public String getUnifiedDiff(DefaultProject project, String absolutePath) {
        throw new UnsupportedOperationException("Not supported yet.");
    }

    @Override public String getUnifiedDiff(DefaultProject project, SVNKitItemInfo itemInfo) {
        throw new UnsupportedOperationException("Not supported yet.");
    }

    @Override protected Set<SVNKitItemInfo> doGetSubItems(SVNKitWrapperConnection connection, DefaultProject project, SVNKitItemInfo itemInfo) {
        throw new UnsupportedOperationException("Not supported yet.");
    }

}
